# Snow CLI User Guide - Games Plugin

## Overview

Snow CLI includes a built-in games panel that you can open with the `/games` command. The panel displays a list of available games — select one and press Enter to start playing.

Snow CLI ships with a built-in Snake game as a reference implementation. You can also write your own game plugins and place them in the user directory. External plugins are automatically loaded and appear in the games list. If an external plugin shares the same `id` as a built-in game, the external plugin overrides the built-in one.

Use this feature when you want to:

- Build terminal mini-games to enjoy during coding breaks
- Learn the Snow CLI plugin system's game-loop architecture
- Replace the built-in Snake game with your own implementation

## Plugin Directory

Snow CLI loads external game plugins from:

```bash
~/.snow/plugin/games/
```

Supported file extensions:

- `.js`
- `.mjs` (recommended for plain ES Modules)
- `.cjs`

Notes:

- Plugins are loaded from the user directory only.
- Snow CLI lazily loads all plugins (sorted by filename) when you enter the games panel.
- Re-open the games panel after adding or modifying a plugin file to load the latest code (no full Snow CLI restart required).
- Built-in games are always available without installing any files.

## Export Formats

A plugin module can export in any of these forms (the loader scans all of them):

```js
export default { ... }
```

```js
export const game = { ... }
```

```js
export const games = [{ ... }, { ... }]
```

If an external plugin's `id` matches a built-in game, the external plugin overrides the built-in one.

## Plugin Structure

Every game plugin must satisfy this shape (TypeScript-style for clarity, but plugin files are plain JavaScript):

```ts
interface GamePlugin<S = unknown> {
	/** Globally unique id for internal indexing */
	id: string;
	/** Display name */
	name: string;
	/**
	 * Short description shown in the games list.
	 * Supports multiple languages: a plain string is used for all languages,
	 * an object selects text by the user's language.
	 */
	description?: string | Partial<Record<'en' | 'zh' | 'zh-TW', string>>;
	/** Author info */
	author?: string;
	/** Version string */
	version?: string;
	/** Enable flag, defaults to true */
	enable?: boolean;

	/** Initialize game state, called once when the game starts */
	init(ctx: GameInitContext): S;
	/** Handle user input, return the updated state */
	handleInput(state: S, input: GameInput): S;
	/** Advance game logic (called every tick), return null for no change */
	tick(state: S): S | null;
	/** Render current state as an array of string lines */
	render(state: S): string[];
	/** Query current game status */
	getStatus(state: S): 'playing' | 'gameover' | 'won' | 'paused';
	/** Get hint text shown at the bottom (optional) */
	getHint?(state: S): string;
	/** Get score text (optional) */
	getScore?(state: S): string | number | null;
}
```

### GameInitContext

The context object received by `init(ctx)`:

```ts
interface GameInitContext {
	/** Terminal width, for games to decide render width */
	terminalWidth: number;
	/** Terminal height, for games to decide render height */
	terminalHeight: number;
}
```

### GameInput

The input object received by `handleInput(state, input)`:

```ts
interface GameInput {
	/** Raw character input */
	input: string;
	/** Key states */
	key: {
		upArrow: boolean;
		downArrow: boolean;
		leftArrow: boolean;
		rightArrow: boolean;
		return: boolean;
		escape: boolean;
		backspace: boolean;
		delete: boolean;
		ctrl: boolean;
		shift: boolean;
		meta: boolean;
	};
}
```

### Multi-language description

The `description` field supports two forms:

**Plain string** (used for all languages):

```js
description: 'A simple terminal game.';
```

**Multi-language object** (automatically selected by the user's language):

```js
description: {
	en: 'A simple terminal game.',
	zh: '一个简单的终端小游戏。',
	'zh-TW': '一個簡單的終端小遊戲。',
}
```

When using a multi-language object, Snow CLI selects the description text in this priority order:

1. The text for the current user language
2. The text for English (`en`)
3. The first available language in the object

This means you don't have to provide translations for every language — just include `en` as a fallback.

## Game Loop

The GameRunner component drives the entire game loop:

```
Game starts
├── Call init(ctx) to initialize state
├── Enter tick loop (default 200ms interval)
│   ├── Call tick(state) to advance logic
│   │   └── Returns null → state unchanged
│   │   └── Returns new state → update state
│   ├── Call render(state) to get the frame
│   ├── Call getStatus(state) to get status
│   ├── Call getHint(state) to get hint (optional)
│   └── Call getScore(state) to get score (optional)
├── User keypress
│   ├── ESC → exit game
│   └── Other keys → call handleInput(state, input)
└── Stop tick loop when status becomes gameover/won
```

### Status Machine

Game status is returned by `getStatus()` and affects render colors:

| Status     | Color  | Meaning             |
| ---------- | ------ | ------------------- |
| `playing`  | Cyan   | Game is in progress |
| `gameover` | Red    | Game is over        |
| `won`      | Green  | Game is won         |
| `paused`   | Yellow | Game is paused      |

## Example: Number Guessing Game

Below is a complete example plugin demonstrating the game loop, input handling, rendering, and scoring:

```js
// ~/.snow/plugin/games/guess-number.mjs

// Internal game state
function initState() {
	return {
		target: Math.floor(Math.random() * 100) + 1,
		guess: null,
		attempts: 0,
		message: '',
		finished: false,
	};
}

export default {
	id: 'example.guess-number',
	name: 'Guess Number',
	description: {
		en: 'Guess a number between 1 and 100.',
		zh: '猜一个 1 到 100 之间的数字。',
		'zh-TW': '猜一個 1 到 100 之間的數字。',
	},
	author: 'Snow CLI',
	version: '1.0.0',
	enable: true,

	init() {
		return initState();
	},

	handleInput(state, input) {
		if (state.finished) {
			if (input.key.return) {
				return initState();
			}
			return state;
		}

		const char = input.input;
		if (char >= '0' && char <= '9') {
			const digit = Number.parseInt(char, 10);
			const current = state.guess === null ? 0 : state.guess;
			const newGuess = current * 10 + digit;
			if (newGuess <= 100) {
				return {...state, guess: newGuess, message: ''};
			}
		}

		if (input.key.return && state.guess !== null) {
			const attempts = state.attempts + 1;
			if (state.guess === state.target) {
				return {
					...state,
					attempts,
					finished: true,
					message: `Correct! You got it in ${attempts} tries.`,
				};
			}
			const hint = state.guess < state.target ? 'Too low!' : 'Too high!';
			return {
				...state,
				attempts,
				guess: null,
				message: hint,
			};
		}

		return state;
	},

	tick(state) {
		return null; // Pure input-driven, no tick logic needed
	},

	render(state) {
		const lines = [];
		lines.push('+-------------------------+');
		lines.push('|   Guess Number (1-100)  |');
		lines.push('+-------------------------+');
		lines.push('');
		lines.push(`  Attempts: ${state.attempts}`);
		lines.push(`  Current:  ${state.guess ?? '---'}`);
		if (state.message) {
			lines.push(`  ${state.message}`);
		}
		lines.push('');
		if (state.finished) {
			lines.push('  Press Enter to play again.');
		} else {
			lines.push('  Type digits, Enter to submit.');
		}
		return lines;
	},

	getStatus(state) {
		return state.finished ? 'gameover' : 'playing';
	},

	getHint(state) {
		if (state.finished) {
			return 'Press Enter to restart, ESC to exit.';
		}
		return 'Type 0-9 to enter a number. Enter to guess. ESC to exit.';
	},

	getScore(state) {
		return `Attempts: ${state.attempts}`;
	},
};
```

## Example: Override Built-in Snake

If you want to replace the built-in Snake game with your own implementation, simply set the plugin `id` to `builtin.snake`:

```js
// ~/.snow/plugin/games/my-snake.mjs

export default {
	id: 'builtin.snake', // Same id overrides the built-in game
	name: 'My Snake',
	description: 'My custom snake implementation.',
	enable: true,

	init(ctx) {
		// Your initialization logic
	},

	handleInput(state, input) {
		// Your input handling
	},

	tick(state) {
		// Your game logic
	},

	render(state) {
		// Your rendering logic
	},

	getStatus(state) {
		// Return status
	},
};
```

## Rendering Notes

- Each string returned by `render()` corresponds to one terminal line. GameRunner renders them line by line with `<Text>`.
- The area above the game canvas shows the game name and score/status label.
- Below the canvas, the hint text from `getHint()` is displayed.
- When the game is over, the canvas turns gray and the status label changes color.
- Press ESC during gameplay to exit back to the menu.

## Writing Your Own Plugin: Checklist

1. **Pick a stable, unique `id`**. Used for internal indexing and overriding built-in games; keep it unchanged once published.
2. **`init()` must return a fresh state object**. Don't cache old state.
3. **`handleInput()` and `tick()` return new state, don't mutate in place**. Follow React's immutable update principle.
4. **`tick()` returns `null` for no change**. Pure input-driven games can always return `null`.
5. **`render()` returns a string array**. Keep each line within terminal width; use `GameInitContext.terminalWidth` for adaptation.
6. **`getStatus()` must correctly reflect game status**. This affects render color and whether the tick loop continues.
7. **`getHint()` and `getScore()` are optional**. Not providing them won't cause errors.
8. **`description` supports multiple languages**. Providing at least `en` as a fallback is recommended.
9. **Plugins run as Node.js modules**. You can `import` any Node.js built-in module and use `process.env` to read environment variables.
10. **`enable: false` temporarily disables a plugin** without deleting the file.

## Troubleshooting

- **Plugin does not appear in the games list.**

  - Make sure the plugin file is in `~/.snow/plugin/games/`.
  - Make sure the file extension is `.js` / `.mjs` / `.cjs`.
  - Check if the plugin has `enable: false`.
  - Make sure your export is an object with `{id, name, init, handleInput, tick, render, getStatus}` — the loader logs `did not export a valid GamePlugin` when validation fails.

- **Game canvas is blank after starting.**

  - Check that `render()` returns a non-empty string array.
  - Check that `init()` correctly returns the initial state.

- **Keys are not responding.**

  - Check that `handleInput()` correctly parses `input.input` (raw character) and `input.key` (key states).
  - Remember that `handleInput()` must return a new state object, not mutate the old one.

- **Game status is incorrect.**

  - Check that `getStatus()` returns the correct status value.
  - `gameover` or `won` status stops the tick loop.

- **I want to override the built-in Snake but it's not working.**

  - Make sure the plugin `id` exactly matches: `builtin.snake`.
  - Make sure the plugin is not disabled with `enable: false`.

## Related Files

- `source/utils/plugins/games/types.ts` — Type definitions
- `source/utils/plugins/games/loader.ts` — Plugin loader
- `source/utils/plugins/games/builtin/snake.ts` — Built-in Snake reference implementation
- `source/ui/pages/GamesScreen.tsx` — Games panel page
- `source/ui/components/games/GameRunner.tsx` — Generic game runner component

## Related

- [Custom StatusLine Guide](./21.Custom%20StatusLine%20Guide.md) — same plugin-loading philosophy applied to the status line
- [Custom Headers Plugin Guide](./26.Custom%20Headers%20Plugin%20Guide.md) — same plugin-loading philosophy applied to custom headers
- [Custom Search Engine Guide](./23.Custom%20Search%20Engine%20Guide.md) — same plugin-loading philosophy applied to search engines
